﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Security.Cryptography;
using LiteExpress.Utilities.CryptoExtension;

namespace LiteExpress.Utilities
{
    public class AES256CBCHelper
    {
        /*  Wanting to stay compatible with NodeJS
         *  http://stackoverflow.com/questions/18502375/aes256-encryption-decryption-in-both-nodejs-and-c-sharp-net/
         *  http://stackoverflow.com/questions/12261540/decrypting-aes256-encrypted-data-in-net-from-node-js-how-to-obtain-iv-and-key
         *  http://stackoverflow.com/questions/8008253/c-sharp-version-of-openssl-evp-bytestokey-method
         *  
         * var cipher = crypto.createCipher('aes-256-cbc', 'passphrase');
         * var encrypted = cipher.update("test", 'utf8', 'base64') + cipher.final('base64');
         * 
         * var decipher = crypto.createDecipher('aes-256-cbc', 'passphrase');
         * var plain = decipher.update(encrypted, 'base64', 'utf8') + decipher.final('utf8');
         */


        //string result2 = CryptoUtility.Encrypt("1231greg23aaa123", "6F5AB30A14AE694CE6E27D50CAC087A5");
        //string result3 = CryptoUtility.Decrypt(result2, "6F5AB30A14AE694CE6E27D50CAC087A5");

        public enum StringEncodeType
        {
            /// <summary>
            /// base64编码
            /// </summary>
            Base64 = 1,

            /// <summary>
            /// 16进制编码
            /// </summary>
            Hex = 2
        }

        public static string Encrypt(StringEncodeType encodeType, string originalText, string passphrase)
        {
            byte[] key, iv;
            DeriveKeyAndIV(RawBytesFromString(passphrase), null, 1, out key, out iv);

            return Encrypt(encodeType, originalText, key, iv);
        }

        public static string Encrypt(StringEncodeType encodeType, string originalText, byte[] key, byte[] iv)
        {
            var encryptedBytes = EncryptStringToBytes(originalText, key, iv);
            string result = "";
            switch (encodeType)
            {
                case StringEncodeType.Base64:
                    result = encryptedBytes.ToBase64();
                    break;
                case StringEncodeType.Hex:
                    result = encryptedBytes.ToHex();
                    break;
                default: break;
            }
            return result;
        }

        public static string Decrypt(StringEncodeType encodeType, string decryptedText, string passphrase)
        {
            byte[] key, iv;
            DeriveKeyAndIV(RawBytesFromString(passphrase), null, 1, out key, out iv);

            return Decrypt(encodeType, decryptedText, key, iv);
        }

        public static string Decrypt(StringEncodeType encodeType, string decryptedText, byte[] key, byte[] iv)
        {
            byte[] encryptedBytes = new byte[] { };
            switch (encodeType)
            {
                case StringEncodeType.Base64:
                    encryptedBytes = decryptedText.Base64ToBytes();
                    break;
                case StringEncodeType.Hex:
                    encryptedBytes = decryptedText.HexToBytes();
                    break;
                default:
                    return string.Empty;
            }

            return DecryptStringFromBytes(encryptedBytes, key, iv);
        }

        #region get key and iv from passphase
        private static byte[] RawBytesFromString(string input)
        {
            var ret = new List<Byte>();

            foreach (char x in input)
            {
                var c = (byte)((ulong)x & 0xFF);
                ret.Add(c);
            }

            return ret.ToArray();
        }

        private static void DeriveKeyAndIV(byte[] data, byte[] salt, int count, out byte[] key, out byte[] iv)
        {
            List<byte> hashList = new List<byte>();
            byte[] currentHash = new byte[0];

            int preHashLength = data.Length + ((salt != null) ? salt.Length : 0);
            byte[] preHash = new byte[preHashLength];

            System.Buffer.BlockCopy(data, 0, preHash, 0, data.Length);
            if (salt != null)
                System.Buffer.BlockCopy(salt, 0, preHash, data.Length, salt.Length);

            MD5 hash = MD5.Create();
            currentHash = hash.ComputeHash(preHash);

            for (int i = 1; i < count; i++)
            {
                currentHash = hash.ComputeHash(currentHash);
            }

            hashList.AddRange(currentHash);

            while (hashList.Count < 48) // for 32-byte key and 16-byte iv
            {
                preHashLength = currentHash.Length + data.Length + ((salt != null) ? salt.Length : 0);
                preHash = new byte[preHashLength];

                System.Buffer.BlockCopy(currentHash, 0, preHash, 0, currentHash.Length);
                System.Buffer.BlockCopy(data, 0, preHash, currentHash.Length, data.Length);
                if (salt != null)
                    System.Buffer.BlockCopy(salt, 0, preHash, currentHash.Length + data.Length, salt.Length);

                currentHash = hash.ComputeHash(preHash);

                for (int i = 1; i < count; i++)
                {
                    currentHash = hash.ComputeHash(currentHash);
                }

                hashList.AddRange(currentHash);
            }
            hash.Clear();
            key = new byte[32];
            iv = new byte[16];
            hashList.CopyTo(0, key, 0, 32);
            hashList.CopyTo(32, iv, 0, 16);
        }
        #endregion

        static byte[] EncryptStringToBytes(string plainText, byte[] Key, byte[] IV)
        {
            // Check arguments. 
            if (plainText == null || plainText.Length <= 0)
                throw new ArgumentNullException("plainText");
            if (Key == null || Key.Length <= 0)
                throw new ArgumentNullException("Key");
            if (IV == null || IV.Length <= 0)
                throw new ArgumentNullException("Key");
            byte[] encrypted;

            Aes aes = Aes.Create("AES");
            aes.Mode = CipherMode.CBC;
            aes.Padding = PaddingMode.PKCS7;
            aes.Key = Key;
            aes.IV = IV;
            ICryptoTransform cTransform = aes.CreateEncryptor();

            var _Source = Encoding.UTF8.GetBytes(plainText);
            encrypted = cTransform.TransformFinalBlock(_Source, 0, _Source.Length);

            // Return the encrypted bytes from the memory stream. 
            return encrypted;

        }

        static string DecryptStringFromBytes(byte[] cipherText, byte[] Key, byte[] IV)
        {
            // Check arguments. 
            if (cipherText == null || cipherText.Length <= 0)
                throw new ArgumentNullException("cipherText");
            if (Key == null || Key.Length <= 0)
                throw new ArgumentNullException("Key");
            if (IV == null || IV.Length <= 0)
                throw new ArgumentNullException("Key");

            // Declare the string used to hold 
            // the decrypted text. 
            string plaintext = null;

            Aes aes = Aes.Create("AES");
            aes.Mode = CipherMode.CBC;
            aes.Padding = PaddingMode.PKCS7;
            aes.Key = Key;
            aes.IV = IV;
            ICryptoTransform cTransform = aes.CreateDecryptor();
            Byte[] originalSrouceData = cTransform.TransformFinalBlock(cipherText, 0, cipherText.Length);

            plaintext = Encoding.UTF8.GetString(originalSrouceData);

            return plaintext;

        }        
    }
}
